查看原文
其他

面试官邪魅一笑:你猜一个 TCP 重置报文的序列号是多少?

米开朗基杨 云原生实验室 2021-05-26

前言

TCP 重置攻击是使用一个单一的数据包来执行的,只有几个字节大小。攻击者制作并发送一个伪造的 TCP 重置包来干扰用户和网站的连接,欺骗通信双方终止 TCP 连接。我们伟大的 xx 长城便运用了这个技术来进行 TCP 关键字阻断[1]

理解 TCP 重置攻击并不需要具备深厚的网络知识功底,只需要一台笔记本就可以对自己进行模拟攻击。本文将会带你了解 TCP 重置攻击的原理,同时会帮助你理解很多关于 TCP 协议的特性。本文主要内容:

  • 回顾 TCP 协议的基础知识
  • 了解 TCP 重置攻击的原理
  • 使用一个简单的 Python 脚本来模拟攻击

下面开始分析 TCP 重置攻击原理。

1. 伟大的 xx 长城是如何利用 TCP 重置攻击的?

这一段略过,原因你懂得,感兴趣的请直接看原文。

2. TCP 重置攻击的工作原理

在 TCP 重置攻击中,攻击者通过向通信的一方或双方发送伪造的消息,告诉它们立即断开连接,从而使通信双方连接中断。正常情况下,如果客户端收发现到达的报文段对于相关连接而言是不正确的,TCP 就会发送一个重置报文段,从而导致 TCP 连接的快速拆卸。

TCP 重置攻击利用这一机制,通过向通信方发送伪造的重置报文段,欺骗通信双方提前关闭 TCP 连接。如果伪造的重置报文段完全逼真,接收者就会认为它有效,并关闭 TCP 连接,防止连接被用来进一步交换信息。服务端可以创建一个新的 TCP 连接来恢复通信,但仍然可能会被攻击者重置连接。万幸的是,攻击者需要一定的时间来组装和发送伪造的报文,所以一般情况下这种攻击只对长连接有杀伤力,对于短连接而言,你还没攻击呢,人家已经完成了信息交换。

从某种意义上来说,伪造 TCP 报文段是很容易的,因为 TCP/IP 都没有任何内置的方法来验证服务端的身份。有些特殊的 IP 扩展协议(例如 IPSec)确实可以验证身份,但并没有被广泛使用。客户端只能接收报文段,并在可能的情况下使用更高级别的协议(如 TLS)来验证服务端的身份。但这个方法对 TCP 重置包并不适用,因为 TCP 重置包是 TCP 协议本身的一部分,无法使用更高级别的协议进行验证。

尽管伪造 TCP 报文段很容易,但伪造正确的 TCP 重置报文段并完成攻击却并不容易。为了理解这项工作的难度,我们需要先了解一下 TCP 协议的工作原理。

3. TCP 协议工作原理

TCP 协议的目标是向客户端发送一份完整的数据副本。例如,如果我的服务器通过 TCP 连接向你的计算机发送我的网站的 HTML,你的计算机的 TCP 协议栈应该能够以我发送的形式和顺序输出 HTML

然而现实生活中我的 HTML 内容并不是按顺序发送的,它被分解成许多小块(称为 TCP 分组),每个小块在网络上被单独发送,并被重新组合成原来发送的顺序。这种重新组合后的输出被称为 TCP 字节流

将分组重建成字节流并不简单,因为网络是不可靠的。TCP分组可能会被丢弃,可能不按发送的顺序到达客户端,也可能会被重复发送、报文损坏等等。因此,TCP 协议的职责是在不可靠的网络上提供可靠的通信。TCP 通过要求连接双方保持密切联系,持续报告它们接收到了哪些数据来实现可靠通信,这样服务端就能够推断出客户端尚未接收到的数据,并重新发送丢失的数据。

为了进一步理解这个过程,我们需要了解服务端和客户端是如何使用序列号(sequence numbers)来标记和跟踪数据的。

TCP 序列号

TCP 协议的通信双方, 都必须维护一个序列号(sequence numbers),对于客户端来说,它会使用服务端的序列号来将接收到的数据按照发送的顺序排列。

当通信双方建立 TCP 连接时,客户端与服务端都会向对方发送一个随机的初始序列号,这个序列号标识了其发送数据流的第一个字节。TCP 报文段包含了 TCP 头部,它是附加在报文段开头的元数据,序列号就包含在 TCP 头部中。由于 TCP 连接是双向的,双方都可以发送数据,所以 TCP 连接的双方既是发送方也是接收方,每一方都必须分配和管理自己的序列号。

确认应答

当接收方收到一个 TCP 报文段时,它会向发送方返回一个 ACK 应答报文(同时将 TCP 头部的 ACK 标志位置 1),这个 ACK 号就表示接收方期望从发送方收到的下一个字节的序列号。发送方利用这个信息来推断接收方已经成功接收到了序列号为 ACK 之前的所有字节。

TCP 头部格式如下图所示:

一个确认应答报文的 TCP 头部必须包含两个部分:

  • ACK 标志位置位 1
  • 包含确认应答号(ACK number)

TCP 总共有 6 个标志位,下文就会讲到其中的 RST 标志位。

TCP 头部包含了多个选项,其中有一个选择确认选项(SACK),如果使用该选项,那么当接收方收到了某个范围内的字节而不是连续的字节时,就会发送 SACK 告知对方。例如,只收到了字节 1000~30004000~5000,但没有收到 3001~3999。为了简单起见,下文讨论 TCP 重置攻击时将忽略选择确认选项。

如果发送方发送了报文后在一段时间内没有收到 ACK,就认为报文丢失了,并重新发送报文,用相同的序列号标记。这就意味着,如果接收方收到了重复的报文,可以使用序列号来判断是否见过这个报文,如果见过则直接丢弃。网络环境是错综复杂的,往往并不是如我们期望的一样,先发送的数据包,就先到达目标主机,反而它很骚,可能会由于网络拥堵等乱七八糟的原因,会使得旧的数据包,先到达目标主机。一般分两种情况:

  1. 发送的数据包丢失了
  2. 发送的数据包被成功接收,但返回的 ACK 丢失了

这两种情况对发送方来说其实是一样的,发送方并不能区分是哪种情况,所以只能重新发送数据包。

只要不频繁重复发送数据,额外的开销基本可以忽略。

为伪造的重置包选择序列号

构建伪造的重置包时需要选择一个序列号。接收方可以接收序列号不按顺序排列的报文段,但这种容忍是有限度的,如果报文段的序列号与它期望的相差甚远,就会被直接丢弃。

因此,一个成功的 TCP 重置攻击需要构建一个可信的序列号。但什么才是可信的序列号呢?对于大多数报文段(除了重置包,即 RST 包)来说,序列号是由接收方的接收窗口大小决定的。

TCP 滑动窗口大小

想象一下,将一台上世纪 90 年代初的古老计算机,连接到现代千兆光纤网络。闪电般快速的网络可以以令人瞠目结舌的速度向这台古老的计算机传送数据,速度远远超过该计算机的处理能力。但并没有什么卵用,因为只有接收方接收并处理了报文,才能认为这个报文已经被收到了。

TCP 协议栈有一个缓冲区,新到达的数据被放到缓冲区中等待处理。但缓冲区的大小是有限的,如果接收方的处理速度跟不上发送方的发送速度,缓冲区就会被填满。一旦缓冲区被填满,多余的数据就会被直接丢弃,也不会返回 ACK。因此一旦接收方的缓冲区有了空位,发送方必须重新发送数据。也就是说,如果接收方的处理速度跟不上,发送方的发送速度再快也没用。

缓冲区到底有多大?发送方如何才能知道什么时候可以一次发送更多的数据,什么时候该一次发送很少的数据?这就要靠 TCP 滑动窗口了。接收方的滑动窗口大小是指发送方无需等待确认应答,可以持续发送数据的最大值。 假设接收方的通告窗口大小为 100,000 字节,那么发送方可以无需等待确认应答,持续发送 100,000 个字节。再假设当发送方发送第 100,000 个字节时,接收方已经发送了前 10,000 个字节的 ACK,这就意味着窗口中还有 90,000 个字节未被确认,发送方还可以再持续发送 10,000 个字节。如果发送了 10,000 个字节的过程中没有收到任何的 ACK,那么接收方的滑动窗口将被填满,发送方将停止发送新数据(可以继续发送之前丢失的数据),直到收到相关的 ACK 才可以继续发送。

TCP 连接双方会在建立连接的初始握手阶段通告对方自己窗口的大小,后续还可以动态调整。TCP 缓冲区大的服务器可能会声明一个大窗口,以便最大限度提高吞吐量。TCP 缓冲区小的服务器可能会被迫声明一个小窗口,这样做会牺牲一定的吞吐量,但为了防止接收方的 TCP 缓冲区溢出,还是很有必要的。

换个角度来看,TCP 滑动窗口大小是对网络中可能存在的未确认数据量的硬性限制。我们可以用它来计算发送方在某一特定时间内可能发送的最大序列号(max_seq_no):

max_seq_no = max_acked_seq_no + window_size

其中 max_acked_seq_no 是接收方发送的最大 ACK 号,它表示发送方知道接收方已经成功接收的最大序列号。window_size 是窗口大小,它表示允许发送方最多发送的未被确认的字节。所以发送方可以发送的最大序列号是:max_acked_seq_no + window_size

TCP 规范规定,接收方应该忽略任何序列号在接收窗口之外的数据。例如,如果接收方确认了所有序列号在 15,000 以下的字节,且接收窗口大小为 30,000,那么接下来接收方只能接收序列号范围在 15,000 ~ 45,000 之间的数据。如果一个报文段的部分数据在窗口内,另一部分数据在窗口外,那么窗口内的数据将被接收确认,窗口外的数据将被丢弃。注意:这里忽略了选择确认选项,再强调一遍!

对于大多数 TCP 报文段来说,滑动窗口的规则告诉了发送方自己可以接收的序列号范围。但对于重置报文来说,序列号的限制更加严格,这是为了抵御一种攻击叫做盲目 TCP 重置攻击(blind TCP reset attack),下文将会解释。

TCP 重置报文段的序列号

对于 TCP 重置报文段来说,接收方对序列号的要求更加严格,只有当其序列号正好等于下一个预期的序列号时才能接收。继续搬出上面的例子,接收方发送了一个确认应答,ACK 号为 15,000。如果接下来收到了一个重置报文,那么其序列号必须是 15,000 才能被接收。

如果重置报文的序列号超出了接收窗口范围,接收方就会直接忽略该报文;如果其序列号在接收窗口范围内,那么接收方就会返回一个 challenge ACK,告诉发送方重置报文段的序列号是错误的,并告之正确的序列号,发送方可以利用 challenge ACK 中的信息来重新构建和发送重置报文。

其实在 2010 年之前,TCP 重置报文段和其他报文段的序列号限制规则一样,但无法抵御盲目 TCP 重置攻击,后来才采取这些措施施加额外的限制。

盲目 TCP 重置攻击

如果攻击者能够截获通信双方正在交换的信息,攻击者就能读取其数据包上的序列号和确认应答号,并利用这些信息得出伪装的 TCP 重置报文段的序列号。相反,如果无法截获通信双方的信息,就无法确定重置报文段的序列号,但仍然可以批量发出尽可能多不同序列号的重置报文,以期望猜对其中一个序列号。这就是所谓的盲目 TCP 重置攻击(blind TCP reset attack)。

在 2010 年之前 TCP 的原始版本中,攻击者只需要猜对接收窗口内的随便哪一个序列号即可,一般只需发送几万个报文段就能成功。采取额外限制的措施后,攻击者需要发送数以百万计的报文段才有可能猜对序列号,这几乎是很难成功的。更多细节请参考 RFC-5963[2]

4. 模拟攻击

以下实验是在 OSX 系统中完成的,其他系统请自行测试。

现在来总结一下伪造一个 TCP 重置报文要做哪些事情:

  • 嗅探通信双方的交换信息。
  • 截获一个 ACK 标志位置位 1 的报文段,并读取其 ACK 号。
  • 伪造一个 TCP 重置报文段(RST 标志位置为 1),其序列号等于上面截获的报文的 ACK 号。这只是理想情况下的方案,假设信息交换的速度不是很快。大多数情况下为了增加成功率,可以连续发送序列号不同的重置报文。
  • 将伪造的重置报文发送给通信的一方或双方,时其中断连接。

为了实验简单,我们可以使用本地计算机通过 localhost 与自己通信,然后对自己进行 TCP 重置攻击。需要以下几个步骤:

  1. 在两个终端之间建立一个 TCP 连接。
  2. 编写一个能嗅探通信双方数据的攻击程序。
  3. 修改攻击程序,伪造并发送重置报文。

下面正式开始实验。

建立 TCP 连接

可以使用 netcat[3] 工具来建立 TCP 连接,这个工很多操作系统都预装了。打开第一个终端窗口,运行以下命令:

$ nc -nvl 8000

这个命令会启动一个 TCP 服务,监听端口为 8000。接着再打开第二个终端窗口,运行以下命令:

$ nc 127.0.0.1 8000

该命令会尝试与上面的服务建立连接,在其中一个窗口输入一些字符,就会通过 TCP 连接发送给另一个窗口并打印出来。

嗅探流量

编写一个攻击程序,使用 Python 网络库 scapy 来读取两个终端窗口之间交换的数据,并将其打印到终端上。完整的代码参考我的 GitHub 仓库[4],代码的核心是调用 scapy 的嗅探方法:

t = sniff(
        iface='lo0',
        lfilter=is_packet_tcp_client_to_server(localhost_ip, localhost_server_port, localhost_ip),
        prn=log_packet,
        count=50)

这段代码告诉 scapylo0 网络接口上嗅探数据包,并记录所有 TCP 连接的详细信息。

  • iface : 告诉 scapy 在 lo0(localhost)网络接口上进行监听。
  • lfilter : 这是个过滤器,告诉 scapy 忽略所有不属于指定的 TCP 连接(通信双方皆为 localhost,且端口号为 8000)的数据包。
  • prn : scapy 通过这个函数来操作所有符合 lfilter 规则的数据包。上面的例子只是将数据包打印到终端,下文将会修改函数来伪造重置报文。
  • count : scapy 函数返回之前需要嗅探的数据包数量。

发送伪造的重置报文

下面开始修改程序,发送伪造的 TCP 重置报文来进行 TCP 重置攻击。根据上面的解读,只需要修改 prn 函数就行了,让其检查数据包,提取必要参数,并利用这些参数来伪造 TCP 重置报文并发送。

例如,假设该程序截获了一个从(src_ip, src_port)发往 (dst_ip, dst_port)的报文段,该报文段的 ACK 标志位已置为 1,ACK 号为 100,000。攻击程序接下来要做的是:

  • 由于伪造的数据包是对截获的数据包的响应,所以伪造数据包的源 IP/Port 应该是截获数据包的目的 IP/Port,反之亦然。
  • 将伪造数据包的 RST 标志位置为 1,以表示这是一个重置报文。
  • 将伪造数据包的序列号设置为截获数据包的 ACK 号,因为这是发送方期望收到的下一个序列号。
  • 调用 scapysend 方法,将伪造的数据包发送给截获数据包的发送方。

对于我的程序而言,只需将这一行[5]取消注释,并注释这一行的上面一行,就可以全面攻击了。按照步骤 1 的方法设置 TCP 连接,打开第三个窗口运行攻击程序,然后在 TCP 连接的其中一个终端输入一些字符串,你会发现 TCP 连接被中断了!

进一步实验

  1. 可以继续使用攻击程序进行实验,将伪造数据包的序列号加减 1 看看会发生什么,是不是确实需要和截获数据包的 ACK 号完全相同。
  2. 打开 Wireshark,监听 lo0 网络接口,并使用过滤器 ip.src == 127.0.0.1 && ip.dst == 127.0.0.1 && tcp.port == 8000 来过滤无关数据。你可以看到 TCP 连接的所有细节。
  3. 在连接上更快速地发送数据流,使攻击更难执行。

总的来说,TCP 重置攻击既深奥又简单,祝你实验顺利。

脚注

[1]

TCP 关键字阻断: https://en.wikipedia.org/wiki/Great_Firewall

[2]

RFC-5963: https://tools.ietf.org/html/rfc5961

[3]

netcat: https://linux.die.net/man/1/nc

[4]

我的 GitHub 仓库: https://github.com/robert/how-does-a-tcp-reset-attack-work/blob/master/main.py

[5]

这一行: https://github.com/robert/how-does-a-tcp-reset-attack-work/blob/77d06123b24a0b69f5ed829bcaeb3db4aa7add8e/main.py#L116-L119

原文链接:https://robertheaton.com/2020/04/27/how-does-a-tcp-reset-attack-work/


你可能还喜欢

点击下方图片即可阅读

什么?VMware Fusion 也能 docker run 了?

云原生是一种信仰 🤘



码关注公众号

后台回复◉k8s◉获取史上最方便快捷的 Kubernetes 高可用部署工具,只需一条命令,连 ssh 都不需要!



点击 "阅读原文" 获取更好的阅读体验!

❤️给个「在看」,是对我最大的支持❤️

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存